﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//    TimeControl.xaml.cs
//
// Abstract:
//    
//    Time control for specifying time periods during the week.
//
//--

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.OutlookAddIn
{
    /// <summary>
    /// Interaction logic for TimeControl.xaml
    /// </summary>
    public partial class TimeControl : UserControl
    {
        private readonly int NumColumns = NotificationTimeCondition.NumberOfDays;
        private readonly int NumRows = NotificationTimeCondition.IntervalsPerDay;
        private readonly int NumCells;
        private const int CellHeight = 25;

        private readonly Brush CellOff = Brushes.LightBlue;
        private readonly Brush CellOn = Brushes.DarkBlue;

        private BitArray _times;
        private List<Rectangle> _cells = new List<Rectangle>();

        private bool _mouseDown;
        private BitArray _selectedCells; // Tracks the current cell selection so a cell is only toggled once during a mouse move.

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <remarks>
        /// The rows and columns for the time grid are created programmatically to make it easier to customize the
        /// control for different interval sizes (e.g. 15 instead of 30 minutes per row).
        /// </remarks>
        public TimeControl()
        {
            Debug.Assert(NumColumns > 0, "NumColumns");
            Debug.Assert(NumRows > 0, "NumRows");

            InitializeComponent();

            NumCells = NumColumns * NumRows;

            // Use the current UI culture to create the day-of-week/time-of-day header for the correct culture.
            CultureInfo cultureInfo = Thread.CurrentThread.CurrentUICulture;

            CreateGridColumns(cultureInfo);
            CreateGridRows(cultureInfo);
        }

        /// <summary>
        /// Create the grid columns with culture-appropriate header text.
        /// </summary>
        private void CreateGridColumns(CultureInfo cultureInfo)
        {
            Debug.Assert(cultureInfo != null, "cultureInfo");

            DateTimeFormatInfo dateTimeFormatInfo = DateTimeFormatInfo.GetInstance(cultureInfo);

            for (int i = 0; i <= NumColumns; i++)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition());

                if (i == 0)
                {
                    continue; // The 0th column has no header and is not included in "NumColumns".
                }

                // Create the header text.
                TextBlock header = new TextBlock();
                header.Style = Helpers.AssertCast<Style>(grid.Resources["DayOfWeek"]);

                Debug.Assert(i - 1 < dateTimeFormatInfo.DayNames.Length, "Day names index exceeds array bounds.");               
                header.Text = dateTimeFormatInfo.DayNames[i - 1];

                Grid.SetColumn(header, i);
                grid.Children.Add(header);
            }
        }

        /// <summary>
        /// Create the grid rows with culture-appropriate header text.
        /// </summary>
        private void CreateGridRows(CultureInfo cultureInfo)
        {
            Debug.Assert(cultureInfo != null, "cultureInfo");
        
            // Create a reference DateTime instance for midnight (00:00 AM) on an arbitrary day.  This
            // will be use to generate the hour header (midnight + row * 30mins).  Note that the
            // short time format includes the "AM"/"PM" (for en-us).  We could strip this out in all 
            // rows except the first row that "AM" and "PM" appear in - seems like overkill for now.
            DateTime midnight = new DateTime(/*year*/2007, /*month*/10, /*day*/7, /*hour*/0, /*minute*/0, /*second*/0);

            int minsPerRow = 60 * 24 / NumRows;
           
            for (int i = 0; i <= NumRows; i++)
            {
                RowDefinition row = new RowDefinition();
                row.Height = new GridLength(CellHeight);
                grid.RowDefinitions.Add(row);
                
                if (i == 0)
                {
                    continue; // The 0th row has no header and is not included in "NumRows".
                }

                // Create the row header text.
                TextBlock header = new TextBlock();
                header.Style = Helpers.AssertCast<Style>(grid.Resources["TimeOfDay"]);
                header.Text = midnight.AddMinutes(minsPerRow * (i - 1)).ToString("t", cultureInfo);
                
                Grid.SetRow(header, i);
                grid.Children.Add(header);
            }
        }

        /// <summary>
        /// Initializes the control with the time array and sets the state of
        /// each cell in the time grid to match its counterpart in the times array.
        /// </summary>
        public void Initialize(BitArray times)
        {          
            Debug.Assert(times != null, "times");
            Debug.Assert(times.Length == NumCells, "Unexpected size of times array");

            _times = times;
            _selectedCells = new BitArray(times.Length);

            for (int columnIndex = 0; columnIndex < NumColumns; columnIndex++)
            {
                for (int rowIndex = 0; rowIndex < NumRows; rowIndex++)
                {
                    Rectangle cell = new Rectangle();
                    cell.Fill = _times[GetTimeIndexFromPosition(rowIndex, columnIndex)] ? CellOn : CellOff;
                    cell.HorizontalAlignment = HorizontalAlignment.Stretch;
                    cell.VerticalAlignment = VerticalAlignment.Stretch;
                    Grid.SetColumn(cell, columnIndex + 1);
                    Grid.SetRow(cell, rowIndex + 1);
                    grid.Children.Add(cell);
                    _cells.Add(cell);
                }
            }
        }

        /// <summary>
        /// Handles left mouse button click on the grid by toggling the cell bit.
        /// </summary>
        private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            _mouseDown = true;
      
            int cellIndex = GetTimeIndexFromPosition(e.GetPosition(grid));

            _selectedCells.SetAll(false); // Clear the list of cells selected by this mouse move.

            ToggleCellAndAddToSelection(cellIndex);

            // Start capturing all mouse events (not just those associated with this grid).  This is needed 
            // if the mouse is dragged off the time control and then released we want to catch that mouse up
            // event and stop cell selection.
            Mouse.Capture(grid);
        }

        /// <summary>
        /// Completes handling of a mouse down/move event sequence.
        /// </summary>
        private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");
           
            _mouseDown = false;

            Mouse.Capture(null); // Release capture of all mouse events.
        }

        /// <summary>
        /// Handles mouse move event to select cells by dragging the mouse over them.
        /// </summary>
        private void Grid_MouseMove(object sender, MouseEventArgs e)
        {
            Debug.Assert(sender != null, "sender");
            Debug.Assert(e != null, "e");

            if (!_mouseDown)
            {
                return; // Ignore as mouse not down.
            }

            Point position = e.GetPosition(grid);

            bool outsideGrid = position.X < 0 || position.X > grid.RenderSize.Width || 
                               position.Y < 0 || position.Y > grid.RenderSize.Height;
            if (outsideGrid)
            {
                return; // Mouse capture is on so we could get events from outside the grid.
            }

            int cellIndex = GetTimeIndexFromPosition(position);

            if (cellIndex < 0 || _selectedCells[cellIndex])
            {
                return; // Don't toggle a cell more than once per mouse drag.
            }

            ToggleCellAndAddToSelection(cellIndex);
        }

        /// <summary>
        /// Switches the color of the specified cell, updates the time array and adds the
        /// cell to the list of cells selected by the current mouse drag operation.
        /// </summary>
        private void ToggleCellAndAddToSelection(int cellIndex)
        {
            // Ignore header (day  or time) cells which have cellIndex of -1.
            if (cellIndex < 0)
            {
                return;
            }

            _selectedCells[cellIndex] = true;

            // Toggle the time bit in the array.
            Debug.Assert(cellIndex < _times.Count, "cellIndex exceeds _times bounds");           
            _times[cellIndex] = !_times[cellIndex];

            // Toggle the background color.
            Debug.Assert(cellIndex < _cells.Count, "cellIndex exceeds _cells bounds");
            Rectangle cell = _cells[cellIndex];
            cell.Fill = cell.Fill == CellOn ? CellOff : CellOn;
        }

        /// <summary>
        /// Converts the co-ordinates of a point on the grid into an index into the _cells/_times array.
        /// </summary>
        /// <returns>
        /// Index into the time array or -1 if the mouse is not inside the grid.
        /// </returns>
        private int GetTimeIndexFromPosition(Point positionInGrid)
        {
            Debug.Assert(positionInGrid != null, "positionInGrid");

            double cellHeight = grid.RenderSize.Height / (NumRows + 1);
            int rowIndex = (int)(positionInGrid.Y / cellHeight);
            --rowIndex; // Exclude the day header row.

            double cellWidth = grid.RenderSize.Width / (NumColumns + 1);
            int columnIndex = (int)(positionInGrid.X / cellWidth);
            --columnIndex; // Exclude the time column.

            return GetTimeIndexFromPosition(rowIndex, columnIndex);
        }

        /// <summary>
        /// Converts the row,column co-ordinates (where 0,0 is midnight, Sunday) into an index 
        /// into the _cells/_times array.
        /// </summary>
        /// <returns>
        /// Index into the time array or -1 if the mouse is not inside the grid.
        /// </returns>
        private int GetTimeIndexFromPosition(int rowIndex, int columnIndex)
        {
            if (rowIndex < 0 || rowIndex >= NumRows ||
                columnIndex < 0 || columnIndex >= NumColumns)
            {
                return -1;
            }

            int index = columnIndex * NumRows + rowIndex;

            Debug.Assert(index >= 0 && index < NumCells, "Index exceeds time array bounds");

            return index;
        }
    }
}
